package com.naszetatry.logic;

import java.sql.Time;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Random;

import com.naszetatry.beans.Edge;
import com.naszetatry.beans.Path;
import com.naszetatry.beans.SearchResult;
import com.naszetatry.beans.Vertex;

public class SearchResultLogic {
	
	private List<Edge> edges;
	private List<Path> paths;
	private List<Vertex> vertexes;
	private List<SearchResult> searchResults;
	private Vertex globalStart;
	
	private Time userTime;
	private float maxDifficulty;
	
	private static final int MAX_TIME = 50400000; 	//14h
	private static final int HOUR = 3600000; 		//1h
	
	private static SearchResultLogic instance = null;
	/**
	 * Number of search results created by once initialized instance of
	 * SearchResultLogic class. It usually will differ from number of
	 * SearchResults returned by search functions. It is used only to
	 * make SearchResult identifiers unique.
	 */
	private static int nr;
	
	/**
	 * Creates an instance of SearchResultLogic. It retrieves actual
	 * lists of edges, paths and vertexes from database used in search procedures.
	 * Variable <code>nr</code> is reset to 1.
	 */
	private SearchResultLogic() {
		edges = EdgeLogic.loadEdges();
		paths = PathLogic.loadPaths();
		vertexes = VertexLogic.loadVertexes();
		searchResults = null;
		globalStart = null;
		
		nr = 1;
		
		userTime = new Time(0);
		maxDifficulty = 5.f;
	}
	
	/**
	 * Creates an instance of SearchResultLogic. It retrieves actual
	 * lists of edges, paths and vertexes from database used in search procedures.
	 * @return
	 * instance of SearchResultLogic
	 */
	public static SearchResultLogic createInstance() {
		instance = new SearchResultLogic();
		return instance;
	}
	
	/**
	 * Gets an instance of SearchResultLogic.
	 * @return
	 * instance of SearchResultLogic
	 */
	public static SearchResultLogic getInstance() {
		return instance;
	}
	
	public void setUserTime(Time ut) {
		userTime = ut;
	}
	
	public void setMaxDifficulty(float md) {
		maxDifficulty = md;
	}
	
	/**
	 * Returns all found routes between <bold>start</bold> and
	 * <bold>end</bold> vertexes. Single path can be used in one route
	 * only once (or twice if it is a 'dead-end'). That prevents from
	 * retrieving nonsense tracks with multi-loops. Start and end can
	 * be the same. 
	 * @param start
	 * - starting vertex
	 * @param end
	 * - ending vertex
	 * @return
	 * all routes that follow specified criteria
	 */
	public List<SearchResult> searchPaths(Vertex start, Vertex end) {
		return searchPaths(start, end, null);
	}
	
	/**
	 * Returns all found routes between <bold>start</bold> and
	 * <bold>end</bold> vertexes. Single path can be used in one route
	 * only once (or twice if it is a 'dead-end'). That prevents from
	 * retrieving nonsense tracks with multi-loops. Start and end can
	 * be the same. 
	 * @param start
	 * - starting vertex
	 * @param end
	 * - ending vertex
	 * @param mid
	 * - check points - list of vertexes that should be "visited" while searching;
	 * all found tracks contain those vertexes; can be <code>null</code>
	 * @return
	 * all routes that follow specified criteria
	 */
	public List<SearchResult> searchPaths(Vertex start, Vertex end, List<Vertex> mid) {
		return searchPaths(start, end, mid, false);
	}
	
	/**
	 * Returns all found routes between <bold>start</bold> and
	 * <bold>end</bold> vertexes. Single path can be used in one route
	 * only once (or twice if it is a 'dead-end'). That prevents from
	 * retrieving nonsense tracks with multi-loops. Start and end can
	 * be the same. 
	 * @param start
	 * - starting vertex
	 * @param end
	 * - ending vertex
	 * @param mid
	 * - check points - list of vertexes that should be "visited" while searching;
	 * all found tracks contain those vertexes;
	 * if <code>mid</code> is <code>null</code>, param <code>order</code> is not used
	 * @param order
	 * - specifies if the check points should be "visited" in the same order like on the list;
	 * if <code>order</code> is <code>false</code> - list order is ignored
	 * @return
	 * all routes that follow specified criteria
	 */
	public List<SearchResult> searchPaths(Vertex start, Vertex end, List<Vertex> mid, boolean order) {
		simpleSearch(null, null, mid, start, end);
		
		if(mid != null && !mid.isEmpty()) {
			filterByCheckPoints(mid, order);
		}
		prepareList();
		
		return searchResults;
	}
	
	private void filterByCheckPoints(List<Vertex> list, boolean order) {
		
		List<SearchResult> srList = new ArrayList<SearchResult>();
		if(!order) {
			Iterator<SearchResult> it = searchResults.iterator();
			while(it.hasNext()) {
				SearchResult sr = it.next();
				if(sr.containsAllCheckPoints(list)) srList.add(sr);
			}
			searchResults = srList;
		} else {
			Iterator<SearchResult> it = searchResults.iterator();
			while(it.hasNext()) {
				SearchResult sr = it.next();
				if(sr.containsOrderedCheckPoints(list)) srList.add(sr);
			}
			searchResults = srList;
		}
	}
	
	@Deprecated
	private void checkPointSearch(Vertex start, Vertex end, List<Vertex> mid) {
		mid.add(end);
		List<SearchResult> alreadyFound = new ArrayList<SearchResult>();
		Iterator<Vertex> itv = mid.iterator();
		while(itv.hasNext()) {
			Vertex vert = itv.next();
			
			simpleSearch(null, null, mid, start, vert);
			alreadyFound = joinLists(alreadyFound);
			if(alreadyFound == null) {
				searchResults = null;
				return;
			}
			start = vert;
			if(start == end) {
				searchResults = alreadyFound;
				return;
			}
		}
		//it shouldn't happen!
		searchResults = null;
	}
	
	@Deprecated
	private List<SearchResult> joinLists(List<SearchResult> af) {
		if(af.isEmpty()) {
			af = searchResults;
		} else {
			if(searchResults.size() < 1) return null;
			if(searchResults.size() > 1) {
				List<SearchResult> temp = new ArrayList<SearchResult>();
				ListIterator<SearchResult> it1 = af.listIterator();
				while(it1.hasNext()) {
					SearchResult base = it1.next();
					ListIterator<SearchResult> it2 = searchResults.listIterator();
					while(it2.hasNext()) {
						SearchResult sr = base.copy();
						SearchResult sr2 = it2.next();
						sr.add(sr2);
						temp.add(sr);
					}
				}
				af = temp;
			} else {
				Iterator<SearchResult> it = af.iterator();
				while(it.hasNext()) {
					it.next().add(searchResults.get(0));
				}
			}
		}
		return af;
	}
	
	private void simpleSearch(List<Edge> used, List<Edge> twiceUsed, List<Vertex> mid, Vertex start, Vertex end) {
		
		//initialize lists
		if(used == null) {
			searchResults = new ArrayList<SearchResult>();
			used = new ArrayList<Edge>();
			twiceUsed = new ArrayList<Edge>();
			globalStart = start;
			if(start.getId() == end.getId() &&
					(mid == null || mid.isEmpty())) return;
		} else {
			//The path has been found
			Edge last = used.get(used.size()-1);
			
			if(last.getEnd() == end.getId() ||
				(last.isTwoWay() && last.getStart() == end.getId())) {
				if(used.size() > 1 || mid == null || mid.isEmpty()) {
					SearchResult sr = new SearchResult();
					initSearchResult(sr, used, globalStart.getId(), end.getId());
					searchResults.add(sr);
					return;
				}
			}
		}
		
		//If this is not the end continue search
		Iterator<Edge> it = edges.iterator();
		while(it.hasNext()) {
			boolean dontUse = false;
			boolean addToTwiceUsed = false;
			Edge e = it.next();
			if(twiceUsed.contains(e)) dontUse = true;
			if(used.contains(e)) {
				/*
				 * If edge is not dead-end, it can produce a cycle.
				 * Don't use such edges, cause it produces weird trails.
				 */
				if(!isDeadEnd(e)) dontUse = true;
				addToTwiceUsed = true;
			}
			if(!dontUse) {
				if(e.getStart() == start.getId()) {
					List<Edge> l1 = copyList(used);
					List<Edge> l2 = copyList(twiceUsed);
					if(addToTwiceUsed) {
						l2.add(e);
					}
					l1.add(e);
					Vertex newStart = new Vertex();
					newStart.setId(e.getEnd());
					simpleSearch(l1, l2, mid, newStart, end);
				}
				if(e.isTwoWay()) {
					if(e.getEnd() == start.getId()) {
						List<Edge> l1 = copyList(used);
						List<Edge> l2 = copyList(twiceUsed);
						if(addToTwiceUsed) {
							l2.add(e);
						}
						l1.add(e);
						Vertex newStart = new Vertex();
						newStart.setId(e.getStart());
						simpleSearch(l1, l2, mid, newStart, end);
					}
				}
			}
		}
	}
	
	private boolean isDeadEnd(Edge edge) {
		int e1 = 0;
		int e2 = 0;
		Iterator<Edge> it = edges.iterator();
		while(it.hasNext()) {
			Edge e = it.next();
			if(e.getStart() == edge.getStart()
					|| e.getEnd() == edge.getStart()) e1++;
			if(e.getStart() == edge.getEnd()
					||	e.getEnd() == edge.getEnd()) e2++;
			if(e1 > 1 && e2 > 1) return false;
		}
		return true;
	}
	
	private void initSearchResult(SearchResult sr, List<Edge> used, int start, int end) {
		ListIterator<Edge> ite = used.listIterator();
		
		List<Path> srPaths = new ArrayList<Path>();
		List<Vertex> srVertexes = new ArrayList<Vertex>();
		
		while(ite.hasNext()) {
			Edge edg = ite.next();
			
			srPaths.add(getPath(edg.getPath()));
			
			Vertex v1 = getVertex(edg.getStart());
			Vertex v2 = getVertex(edg.getEnd());		
			
			if(srVertexes.isEmpty()) {
				boolean normalOrder = true;
				if(edg.isTwoWay()) {
					if(v2.getId() == start) normalOrder = false;
				}
				if(normalOrder) {
					srVertexes.add(v1);
					srVertexes.add(v2);
				} else {
					srVertexes.add(v2);
					srVertexes.add(v1);
				}
			} else {
				boolean normalOrder = true;
				if(edg.isTwoWay()) {

					if(v1.getId() != srVertexes.get(srVertexes.size()-1).getId())
						normalOrder = false;
				}
				
				if(normalOrder) {
					srVertexes.add(v2);
				} else {
					srVertexes.add(v1);
				}
			}
		}
		sr.setPaths(srPaths);
		sr.setVertexes(srVertexes);
		calculate(sr);
	}
	
	private void calculate(SearchResult sr) {
		Iterator<Path> itp = sr.getPaths().iterator();
		
		Time totalTime = new Time(0);
		
		float maxDifficulty = 0.f;
		float avgDifficulty = 0.f;
		float avgAttractivity = 0.f;
		
		int minHeight = 20000;
		int maxHeight = 0;
		
		int count = 0;
		
		while(itp.hasNext()) {
			Path p = itp.next();
			
			if(maxDifficulty < p.getDifficulty()) maxDifficulty = p.getDifficulty();
			avgDifficulty += p.getDifficulty();
			avgAttractivity += p.getAttractivity();
			
			count++;
			
			int m = totalTime.getMinutes() + p.getTime().getMinutes();
			int h = m/60;
			m %= 60;
			h += totalTime.getHours() + p.getTime().getHours();
			totalTime.setHours(h);
			totalTime.setMinutes(m);
		}
		avgDifficulty = avgDifficulty / (float) count;
		sr.setMaxDifficulty(maxDifficulty);
		sr.setAvgDifficulty(avgDifficulty);
		sr.setTotalTime(totalTime);
		sr.setId(nr++);
		
		Iterator<Vertex> itv = sr.getVertexes().iterator();
		while(itv.hasNext()) {
			Vertex v = itv.next();
			avgAttractivity += v.getAttractivity();
			if(minHeight > v.getHeight()) minHeight = v.getHeight();
			if(maxHeight < v.getHeight()) maxHeight = v.getHeight();
			count++;
		}
		avgAttractivity = avgAttractivity / (float) count;
		sr.setAvgAttractivity(avgAttractivity);
		
		sr.setTotalHeight(maxHeight - minHeight);
	}
	
	private Path getPath(int id) {
		Iterator<Path> itp = paths.iterator();
		
		while(itp.hasNext()) {
			Path pth = itp.next();
			if(pth.getId() == id) {
				return pth;
			}
		}
		return null;
	}
	
	private Vertex getVertex(int id) {
		Iterator<Vertex> itv = vertexes.iterator();
		
		while(itv.hasNext()) {
			Vertex vrt = itv.next();
			if(vrt.getId() == id) {
				return vrt;
			}
		}
		return null;
	}
	
	private List<Edge> copyList(List<Edge> list) {
		List<Edge> copy = new ArrayList<Edge>();
		Iterator<Edge> it = list.iterator();
		while(it.hasNext()) copy.add(it.next());
		return copy;
	}
	
	private void prepareList() {
		List<SearchResult> list = new ArrayList<SearchResult>();
		Iterator<SearchResult> it = searchResults.iterator();
		while(it.hasNext()) {
			SearchResult sr = it.next();
			if(sr.getMaxDifficulty() > maxDifficulty) continue;
			//if(sr.getTotalTime().getTime() > MAX_TIME) continue;
			if(sr.getTotalTime().getTime() > userTime.getTime()+HOUR) {
				sr.setColour(SearchResult.RED);
			} else if(sr.getTotalTime().getTime() < userTime.getTime()-HOUR) {
				sr.setColour(SearchResult.BLUE);
			} else {
				sr.setColour(SearchResult.GREEN);
			}
			list.add(sr);
		}
		//Collections.sort(list, new SortByColorAndTime());
		Collections.sort(list, new SortByTime());
		searchResults = list;
	}
	
	public static void main(String[] args) {
		System.out.println("------------------------------------");
		SearchResultLogic srl = SearchResultLogic.createInstance();
		srl.setUserTime(new Time(35000000)); //10h
		Vertex start = new Vertex();
		Vertex end = new Vertex();
		long seed = new Date().getTime();
		Random r = new Random(seed);
		start.setId(1);
		end.setId(10);
		List<Vertex> vert = new ArrayList<Vertex>();
		Vertex v = new Vertex();
		v.setId(14);
		vert.add(v);
		Vertex v2 = new Vertex();
		v2.setId(16);
		vert.add(v2);
		
		System.out.println(start.getId());
		System.out.println(end.getId());
		List<SearchResult> list = srl.searchPaths(start, end, null, true);
		Iterator<SearchResult> it = list.iterator();
		while(it.hasNext()) {
			System.out.println("---------------------------------");
			SearchResult sr = it.next();
			System.out.println("\npaths:");
			Iterator<Path> itp = sr.getPaths().iterator();
			while(itp.hasNext()) {
				System.out.print(itp.next().getId() + ", ");
			}
			System.out.println("\nvertexes:");
			Iterator<Vertex> itv = sr.getVertexes().iterator();
			while(itv.hasNext()) {
				System.out.print(itv.next().getId() + ", ");
			}
			System.out.println("\nTime: " + sr.getTotalTime());
			System.out.println("\nColour: " + sr.getColour());
		}
	}
}
